﻿#region License statement
// NJamb, a specification and delta-specification DSL
// Copyright (c) 2010-2011, Mark Knell
// Published under the MIT License; all other rights reserved
#endregion

#region using...
using System;
using JetBrains.Annotations;
using NJamb.Parser.SpecificationBuilders.Delta;
using NJamb.Parser.Subjects;
using Stile.Patterns.Disposables;
using Stile.Patterns.SelfDescribingPredicates;
using Stile.Readability;
using Stile.Types;
#endregion

namespace NJamb.Specifications.Simple
{
	public abstract class Specification : Disposable
	{
		protected static class Default
		{
			public static readonly Lazy<string> LazyReason = Null.String.ToLazy();
		}
	}

	public abstract class SpecificationBase<TSubject, TCollaboratingSubject, TEvaluation> : Specification,
		IEvaluable<TEvaluation>
		where TCollaboratingSubject : class, ICollaboratingSubject
		where TEvaluation : class, IEvaluation
	{
		private readonly bool _hasExceptionFilter;
		private Func<Exception, TSubject, TEvaluation> _exceptionFilter;

		protected SpecificationBase(TCollaboratingSubject collaboratingSubject,
			Lazy<string> lazyReason = null,
			Func<Exception, TSubject, TEvaluation> exceptionFilter = null)
		{
			CollaboratingSubject = collaboratingSubject;
			LazyReason = lazyReason ?? Default.LazyReason;
			_hasExceptionFilter = InitExceptionFilter(exceptionFilter, out _exceptionFilter);
		}

		public TCollaboratingSubject CollaboratingSubject { get; private set; }
		public Lazy<string> LazyReason { get; private set; }

		public TEvaluation Evaluate()
		{
			RequireNotAlreadyDisposed();

			TEvaluation evaluation;
			TSubject subject = default(TSubject);
			try
			{
				evaluation = GetEvaluation(out subject);
			}
			catch (Exception e)
			{
				TEvaluation filterEvaluation = _exceptionFilter.Invoke(e, subject);
				if (filterEvaluation != null)
				{
					return filterEvaluation;
				}
				throw;
			}

			if (_hasExceptionFilter)
			{
				// exception was expected but none was thrown
				return _exceptionFilter.Invoke(null, subject);
			}
			return evaluation;
		}

		protected override void Disposing()
		{
			CollaboratingSubject = null;
			LazyReason = null;
			_exceptionFilter = null;
		}

		protected string Format(string explanation)
		{
			string because = null;
			if (!string.IsNullOrEmpty(LazyReason.Value))
			{
				because = string.Format("because {0},{1}", LazyReason.Value, Environment.NewLine);
			}
			return string.Format("{0}expected {1} {2}.", because, CollaboratingSubject.Description, explanation);
		}

		protected abstract TEvaluation GetEvaluation(out TSubject subject);

		private static bool InitExceptionFilter([CanBeNull] Func<Exception, TSubject, TEvaluation> exceptionFilter,
			out Func<Exception, TSubject, TEvaluation> exceptionFilterToUse)
		{
			bool hasExceptionFilter = exceptionFilter != null;
			Func<Exception, TSubject, TEvaluation> defaultExceptionFilter = (e, subject) => null;
			exceptionFilterToUse = hasExceptionFilter ? exceptionFilter : defaultExceptionFilter;
			return hasExceptionFilter;
		}
	}

	public abstract class Specification<TSubject> :
		SpecificationBase<TSubject, ICollaboratingSubject<TSubject>, IEvaluation<TSubject>>,
		ICollaboratingSimpleSpecification<TSubject>
	{
		protected Specification(ICollaboratingSubject<TSubject> collaboratingSubject,
			ISelfDescribingPredicate<TSubject> selfDescribingPredicate,
			Lazy<string> lazyReason = null,
			Func<Exception, TSubject, IEvaluation<TSubject>> exceptionFilter = null)
			: base(collaboratingSubject, lazyReason, exceptionFilter)
		{
			SelfDescribingPredicate = selfDescribingPredicate;
		}

		public ISelfDescribingPredicate<TSubject> SelfDescribingPredicate { get; private set; }
		public ICollaboratingSimpleSpecification<TSubject> Xray
		{
			get { return this; }
		}

		protected override void Disposing()
		{
			base.Disposing();
			SelfDescribingPredicate = null;
		}

		protected override IEvaluation<TSubject> GetEvaluation(out TSubject subject)
		{
			subject = CollaboratingSubject.Get();
			return SelfDescribingPredicate.Evaluate(subject, Format);
		}
	}

	public abstract class Specification<TSubject, TCollaboratingSubject, TSpecificationBuilder> : Specification<TSubject>,
		ISimpleSpecification<TSubject, TSpecificationBuilder>
		where TCollaboratingSubject : class, ICollaboratingSubject<TSubject>
		where TSpecificationBuilder : class, IDeltaSpecificationBuilder
	{
		protected Specification(TCollaboratingSubject collaboratingSubject,
			ISelfDescribingPredicate<TSubject> selfDescribingPredicate,
			Lazy<string> lazyReason,
			Func<Exception, TSubject, IEvaluation<TSubject>> exceptionFilter = null)
			: base(collaboratingSubject, selfDescribingPredicate, lazyReason, exceptionFilter)
		{
			LazySpecificationBuilder = new Lazy<TSpecificationBuilder>(Factory);
		}

		public TSpecificationBuilder AndLater
		{
			get
			{
				RequireNotAlreadyDisposed();
				return LazySpecificationBuilder.Value;
			}
		}

		public Lazy<TSpecificationBuilder> LazySpecificationBuilder { get; private set; }

		protected override void Disposing()
		{
			base.Disposing();
			LazySpecificationBuilder = null;
		}

		protected abstract TSpecificationBuilder Factory();
	}
}
